// ReSharper disable InconsistentNaming
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Controls;
using CI.Mesopotamia;
using CI.Utils;
using Microsoft.Ccr.Core;
using Microsoft.Dss.Core.Attributes;
using Microsoft.Dss.ServiceModel.Dssp;
using Microsoft.Dss.ServiceModel.DsspServiceBase;
using System.Drawing;
using CI.Interfaces;
using CI.Organism;

#region Simulation namespaces

using Microsoft.Robotics.Simulation.Engine;

using engineproxy = Microsoft.Robotics.Simulation.Engine.Proxy;
using drive = Microsoft.Robotics.Services.Simulation.Drive.Proxy;
using bumper = Microsoft.Robotics.Services.Simulation.Sensors.Bumper.Proxy;
using simulatedwebcam = Microsoft.Robotics.Services.Simulation.Sensors.SimulatedWebcam.Proxy;


using W3C.Soap;
using System.Threading;
#endregion

namespace CI.MSRoboticsEnv
{
    [DisplayName("Mesopotamia Simulation")]
    [Description("Hosts Mesopotamia Simulation Environment")]
    [Contract(Contract.Identifier)]
    [Serializable]
    public class MSRoboticsEnvService : DsspServiceBase, IHostingEnvironmentPhysical
    {
        #region Properties and private varibles

        // partner attribute will cause simulation engine service to start
        [Partner("Engine", Contract = engineproxy.Contract.Identifier, CreationPolicy = PartnerCreationPolicy.UseExistingOrCreate)]
        private readonly engineproxy.SimulationEnginePort _engineServicePort = new engineproxy.SimulationEnginePort();

        // Main service port
        [ServicePort("/Mesopotamia", AllowMultipleInstances = false)]
        private readonly MSRoboticsEnvOperations _mainPort = new MSRoboticsEnvOperations();
        public MSRoboticsEnvService(DsspServiceCreationPort creationPort) : base(creationPort) { }


        private readonly TimeSpan _startServiceTimeout = new TimeSpan(0, 0, 30);

        // manifest loading pause
        private const int SIMULATION_LOADING_MANIFEST_PAUSE = 1000;
        // simulation state
        private Microsoft.Robotics.Simulation.Proxy.SimulationState _simulationState;

        private readonly Point _externalWindowLocation = new Point(0, 0);
        private readonly Size _externalWindowSize = new Size(400, 400);
        private SimIdleManager simIdleManager;
        private VisualEntityManager visualEntityManager;

        /// <summary>
        /// Max entitys supported by environment
        /// </summary>
        public int MaxEntitysSupported
        {
            get { return 10; }
        }

        /// <summary>
        /// Entitys consumed
        /// </summary>
        public int EntitysConsumed
        {
            get { return visualEntityManager.CalculateEntitysConsumed(); }
        }

        private UserControl _displayControl;
        public UserControl DisplayControl
        {
            get
            {
                return _displayControl ?? (_displayControl = new MudMap());
            }
        }

        /// <summary>
        /// Simuation environment
        /// </summary>
        private SimEnv _simEnv;

        #endregion

        /// <summary>
        /// Show a external camera window if it supports it
        /// </summary>
        public void HideExternalCameraWindow(bool hide)
        {
            _simEnv.TraceLogger.LogDebug("Hiding external Window : " + hide);

            // hide the simulator
            var config = new SimulatorConfiguration
                             {
                                 Headless = hide,
                                 MainWindowRectangle = new Rectangle(_externalWindowLocation, _externalWindowSize)
                             };

            SimulationEngine.GlobalInstancePort.Update(config);
        }

        /// <summary>Register the cell in the environment</summary>
        public bool RegisterCell(ICell cell, ActionType actionType)
        {
            // dump whats going on
            _simEnv.TraceLogger.LogDebug(string.Format("Sim:Organism with GUID {2} with action {1} Cell {0} of type {3}", cell.Guid, actionType, cell.Organism.Guid, cell.GetType()));

            // wait for idle
            WaitForIdle();

            // test code to see if it fixes the timeout issues

            // if its add cell
            switch (actionType)
            {
                case ActionType.AddCell:
                    // if it doesnt already exist, eg mutated genes could cause it to try and add again
                    if (!visualEntityManager.ContainsEntity(cell.Organism.Guid))
                    {
                        // if Amoeba
                        if (cell is Organism.Amoeba)
                            AddAmoeba(cell.Organism.Location, cell.Organism);
                        else if (cell is Organism.iRobot)
                            AddiRobot(cell.Organism.Location, cell.Organism);
                        else
                            throw new SimulationException("Unknown cell type");
                    }
                    else
                        // terminate it
                        cell.Organism.TerminateAndDestroy("Attempting to add duplicate Organism to visual environment");
                    break;

                case ActionType.RemoveCell:
                    lock (this)
                    {
                        // remove the cell from the environment
                        // first make sure it exists, it is possible through mutation that we may get a remove cell command before a add cell
                        // has been executed or even exists, so make sure its their first!
                        if (visualEntityManager.ContainsEntity(cell.Organism.Guid))
                        {
                            // delete entity
                            visualEntityManager.DeleteEntity(cell.Organism.Guid);
                        }
                        else
                        {
                            throw new SimulationException("Unable to locate cell in robotics to action");
                        }
                    }
                    break;

                case ActionType.DelCell:
                    // change the cell to the dead cell color
                    // todo : change cell colour
                    //((VisualEntity)_entitys[cell.Organism.guid.ToString()]).State.Assets.
                    //throw new NotImplementedException();
                    break;

                default:
                    throw new SimulationException("Unknown action type");
            }

            return true;
        }

        /// <summary>Checks to see if the Organism is registered in the physical environment</summary>
        public bool Registered(IOrganism organism)
        {
            return visualEntityManager.ContainsEntity(organism.Guid);
        }

        /// <summary>
        /// Clean up the environment in case of restart
        /// </summary>
        public void CleanUp()
        {
            visualEntityManager.CleanUp();
        }

        /// <summary>Add a Amoeba</summary>
        void AddAmoeba(ILocation location, IOrganism organism)
        {
            _simEnv.TraceLogger.LogDebug(string.Format("Start - Adding Organism {0}", organism.Guid));

            // create amoeba
            var amoeba = new Amoeba(location, organism);

            visualEntityManager.InsertEntity(organism, amoeba);

            ((VisualEntity)amoeba.BaseObject).SuspendUpdates = false;

            _simEnv.TraceLogger.LogDebug(string.Format("Finishing - Adding Organism {0}", organism.Guid));
        }

        /// <summary>Add a iRobot</summary>
        void AddiRobot(ILocation location, IOrganism organism)
        {
            _simEnv.TraceLogger.LogDebug(string.Format("Start - Adding Organism {0}", organism.Guid));

            // create a irobot & the ctor suspends updates
            var irobot = new iRobot(location, organism);

            visualEntityManager.InsertEntity(organism, irobot);

            var robotBase = ((VisualEntity)irobot.BaseObject);

            // if init add other services
            if (robotBase.HasBeenInitialized)
            {
                // Start simulated motor service
                if (!CreateAndStartService(drive.Contract.Identifier, robotBase.State.Name))
                    return;

                // start simulated bumper service
                if (!CreateAndStartService(bumper.Contract.Identifier, irobot.BumperArray.State.Name))
                    return;
                if (!SubscribeiRobotBumpers(organism, irobot))
                    return;

                // start simulated camera service
                if (!CreateAndStartService(simulatedwebcam.Contract.Identifier, irobot.Camera.State.Name))
                    return;
                if (!SubscribeiRobotCamera(organism, irobot))
                    return;
            }

            robotBase.SuspendUpdates = false;

            _simEnv.TraceLogger.LogDebug(string.Format("Finishing - Adding Organism {0}", organism.Guid));

        }

        public void ShutDownService()
        {
            _engineServicePort.Post(new DsspDefaultDrop(DropRequestType.Instance));
            _mainPort.Post(new DsspDefaultDrop(DropRequestType.Instance));
        }

        private bool CreateAndStartService(string contractIdentifier, string entityName)
        {

            var startServiceEvent = new ManualResetEvent(false);
            var result = true;

            DsspResponsePort<CreateResponse> resultPort = CreateService(
                contractIdentifier,
                Microsoft.Robotics.Simulation.Partners.CreateEntityPartner(
                    "http://localhost/" + entityName),
                    Microsoft.Robotics.Simulation.Partners.CreateEntityPartner(Contract.Identifier));


            //asynchronously handle service creation result.
            Activate(Arbiter.Choice(resultPort,
                                    rsp =>
                                    {
                                        _simEnv.TraceLogger.LogDebug(string.Format("Started {0} Service", entityName));

                                        if (startServiceEvent.SafeWaitHandle.IsClosed)
                                            return;

                                        startServiceEvent.Set();
                                    },
                                    fault =>
                                    {
                                        _simEnv.TraceLogger.LogException("Exception", fault.ToException());
                                        _simEnv.TriggerScenarioRestart(fault.ToException().Message);

                                        if (startServiceEvent.SafeWaitHandle.IsClosed)
                                            return;

                                        startServiceEvent.Set();
                                        result = false;
                                    }));

            _simEnv.TraceLogger.LogDebug(string.Format("Waiting - starting service {0}", contractIdentifier));
            if (!startServiceEvent.WaitOne(_startServiceTimeout))
            {
                const string errorMsg = "Error waiting on STARTING SERVICE sync";
                _simEnv.TraceLogger.LogWarn(errorMsg);
                _simEnv.TriggerScenarioRestart(errorMsg);
                result = false;
            }

            startServiceEvent.Close();
            return result;
        }

        private bool SubscribeiRobotBumpers(IOrganism organism, iRobot irobot)
        {
            bool result = false;
            try
            {
                // subscribe to the bumper services
                irobot.SubscribeBumbers(this);
                result = true;
            }
            // if their is a failure subscribing to the bumpers it will terminate the organism
            catch (iRobot.iRobotException rEx)
            {
                LogAndExit(irobot, organism, "Bumper", rEx);
            }
            return result;

        }

        private void LogAndExit(IPhysicalEnvironment physicalEntity, IOrganism organism, string entityName, iRobot.iRobotException rEx)
        {
            _simEnv.TraceLogger.LogDebug(string.Format("{2} HasBeenInitialized failed on {0} with GUID {1}", organism, organism.Guid, entityName));
            // error subscribing to service
            organism.TerminateAndDestroy("Fatal Error subscribing to " + entityName + " service  of organism with guid :" + organism.Guid);

            visualEntityManager.AddBadEntity(organism.Guid, physicalEntity);

            var errorMsg = "Failure subscribing to " + entityName;

            // this is a safe exception that happens occasionly we can ignore it
            _simEnv.TraceLogger.LogException(errorMsg, rEx);

            // set to restart
            _simEnv.TriggerScenarioRestart(errorMsg);
        }

        private bool SubscribeiRobotCamera(IOrganism organism, iRobot irobot)
        {
            bool result = false;
            try
            {
                // subscribe to the camera services
                irobot.SubscribeCamera(this);
                result = true;
            }
            // if their is a failure subscribing to the bumpers it will terminate the organism
            catch (iRobot.iRobotException rEx)
            {
                LogAndExit(irobot, organism, "Camera", rEx);
            }
            return result;
        }

        /// <summary>
        /// Service Start
        /// </summary>
        protected override void Start()
        {
            _simEnv = SimEnv.Instance;

            _simEnv.TraceLogger.LogWarn("MSRoboticsEnv - start");
            base.Start();

            // save hosting environment
            _simEnv.HostEnvPhysical = this;

            HideExternalCameraWindow(true);

            simIdleManager = new SimIdleManager(RequestFPSUpdate);

            visualEntityManager = new VisualEntityManager(
                (entity, mre) => SpawnIterator(entity, mre, SpawnedDeleteEntity),
                (entity, mre) => SpawnIterator(entity, mre, SpawnedInsertEntity),
                simIdleManager);

            _simEnv.TraceLogger.LogDebug("Loading Manifest - Start");

            // save the count of how many base entitys their are
            // wait until it is running
            do
            {
                do
                {
                    simIdleManager.WaitForIdle();

                    // check for break
                    // check to make sure we havent broke
                    if (_simEnv.BreakExit)
                    {
                        _simEnv.TraceLogger.LogDebug("Startup Pause - BreakExit set, aborting thread");
                        // use a mutex to sync between 
                        Semaphore.OpenExisting("MSRB").Release(1);
                        // abort
                        program.Exit(0);
                    }


                } while (_simulationState == null);

                // if the manifest is not loaded yet
                if (simIdleManager.SimulationEntityFPS == 0 || visualEntityManager.SimulationEntityCount == 0)
                    StatsLogger.Sleeper(SIMULATION_LOADING_MANIFEST_PAUSE);

            } while ((simIdleManager.SimulationEntityFPS == 0) || (visualEntityManager.SimulationEntityCount == 0));

            _simEnv.TraceLogger.LogDebug("Loading Manifest - Finish");

            // save the count
            visualEntityManager.SetEmptyEntityCount();

            // use a mutex to sync between 
            Semaphore.OpenExisting("MSRB").Release(1);

            _simEnv.TraceLogger.LogDebug("MSRoboticsEnv - finish");
        }

        /// <summary>
        /// Break out of the hosting environment, usually call when shutting down
        /// Reset the waits
        /// </summary>
        public void Break()
        {
            _simEnv.TraceLogger.LogDebug("Break Command executed");
        }

        public ILocation GetLocation(IOrganism organism)
        {

            ILocation location = new Location();

            var entity = visualEntityManager.GetWithGuidKey(organism.Guid);

            if (entity != null)
            {
                _simEnv.TraceLogger.LogDebug(string.Format("Getting loc for org {0}", organism.Guid));
                // populate the fields
                location.XAxis = ((VisualEntity)entity.BaseObject).Position.X;
                location.YAxis = ((VisualEntity)entity.BaseObject).Position.Y;
                location.ZAxis = ((VisualEntity)entity.BaseObject).Position.Z;
            }

            return location;

        }

        /// <summary>Sets the location of the Organism in the environment</summary>
        public ILocation SetLocation(IOrganism organism, ILocation location)
        {
            return null;
        }

        /// <summary>
        /// Bind a name of a IO Event in the physical world to a synapse
        /// eg "Left Bumper" to a IO synapse, "Right Drive" to a IO synapse
        /// </summary>
        /// <param name="organism">organism</param>
        public void BindSynapseIO(IOrganism organism)
        {
            #region Argument Checks
            if (organism == null)
                throw new ArgumentNullException("Organism");
            #endregion

            var entity = visualEntityManager.GetWithGuidKey(organism.Guid);

            if (entity != null)
                entity.BindSynapseIO();
        }

        /// <summary>
        /// UnBind a name of a IO Event in the physical world to a synapse
        /// eg "Left Bumper" to a IO synapse, "Right Drive" to a IO synapse
        /// </summary>
        /// <param name="organism">Organism its being unbound from</param>
        public void UnBindSynapseIO(IOrganism organism)
        {
            #region Argument Checks
            if (organism == null)
                throw new ArgumentNullException("Organism");
            #endregion

            var entity = visualEntityManager.GetWithGuidKey(organism.Guid);

            if (entity != null)
                entity.UnBindSynapseIO();
        }

        private IEnumerator<ITask> SpawnedInsertEntity(IPhysicalEnvironment visualEntity, ManualResetEvent insertResetEvent)
        {
            if (visualEntity == null)
                yield break;

            SimulationEngine.GlobalInstancePort.Insert(((VisualEntity)visualEntity.BaseObject));

            var faultOrFound = SimulationEngine.GlobalInstancePort.Query(((VisualEntity)visualEntity.BaseObject));
            yield return faultOrFound.Choice();

            // if its a fault exit
            var fault = (Fault)faultOrFound;
            if (fault != null)
            {
                string errorMsg = string.Format("Error Init {0} : {1}", visualEntity.GetType(), visualEntity.Organism.Guid);

                if ((fault.Node == null) && (fault.Code == null) && (fault.Detail == null) && (fault.Reason == null) && (fault.Role == null))
                {
                    _simEnv.TraceLogger.LogWarn(errorMsg);
                }
                else
                {
                    _simEnv.TraceLogger.LogException(errorMsg, fault.ToException());
                }

                _simEnv.TriggerScenarioRestart(errorMsg);
                if (!insertResetEvent.SafeWaitHandle.IsClosed)
                    insertResetEvent.Set();
                yield break;
            }

            var found = (QuerySimulationEntityResponseType)faultOrFound;
            if (found != null)
            {

                _simEnv.TraceLogger.LogDebug("Added " + visualEntity.GetType());
                if (!insertResetEvent.SafeWaitHandle.IsClosed)
                    insertResetEvent.Set();
            }
        }

        private IEnumerator<ITask> SpawnedDeleteEntity(VisualEntity entity, ManualResetEvent deletemanualResetEvent)
        {
            if (entity == null)
                yield break;

            SimulationEngine.GlobalInstancePort.Delete(entity);

            var faultOrFound = SimulationEngine.GlobalInstancePort.Query(entity);
            yield return faultOrFound.Choice();

            // if its a fault exit
            var fault = (Fault)faultOrFound;
            if (fault != null)
            {
                if ((fault.Node == null) && (fault.Code == null) && (fault.Detail == null) && (fault.Reason == null) && (fault.Role == null))
                {
                    _simEnv.TraceLogger.LogDebug("Entity deleted");
                    // signal as being done
                    deletemanualResetEvent.Set();
                }
                else
                {
                    const string restartMsg = "Error Deleting organism";
                    _simEnv.TraceLogger.LogException(restartMsg, fault.ToException());
                    // restart on error
                    _simEnv.TriggerScenarioRestart(restartMsg);
                    deletemanualResetEvent.Set();
                }
                yield break;
            }

            var found = (QuerySimulationEntityResponseType)faultOrFound;
            if (found != null)
            {
                string restartMsg = "Error deleting : " + found.Entity.State.Name;
                _simEnv.TraceLogger.LogDebug(restartMsg);

                // restart on error
                _simEnv.TriggerScenarioRestart(restartMsg);
                deletemanualResetEvent.Set();
                yield break;

            }


            yield break;
        }

        /// <summary>
        /// Update the simulation engine stats
        /// </summary>
        /// <returns></returns>
        IEnumerator<ITask> UpdateEngineState()
        {
            _simEnv.TraceLogger.LogDebug("Updating Engine State Phase 1 - Check for null engine port");

            if (_engineServicePort == null)
                yield break;

            _simEnv.TraceLogger.LogDebug("Updating Engine State Phase 2 - Get from engine port");

            var stateOrFault = _engineServicePort.Get();
            yield return stateOrFault.Choice();

            _simEnv.TraceLogger.LogDebug("Updating Engine State Phase 3 - Check for Fault or value");
            // if its a fault exit
            var fault = (Fault)stateOrFault;
            if (fault != null)
            {

                const string errorMsg = "Error updating system state";
                _simEnv.TraceLogger.LogException(errorMsg, fault.ToException());

                // restart simulation
                _simEnv.TriggerScenarioRestart(errorMsg);

                yield break;
            }

            _simEnv.TraceLogger.LogDebug("Updating Engine State Phase 4 - saving value");
            // else update simulation state
            _simulationState = stateOrFault;
            simIdleManager.SimulationEntityFPS = _simulationState.FramesPerSecond;

            // todo  - fix  - if we are still init to avoid thread issues dont update if zero
            // update the entity count
            if ((_simulationState.SerializedEntities != null) && (_simulationState.SerializedEntities.XmlNodes.Count != 0))
                visualEntityManager.SimulationEntityCount = _simulationState.SerializedEntities.XmlNodes.Count;

            _simEnv.TraceLogger.LogDebug("Updating Engine State Phase 5 - finished");

            _simEnv.TraceLogger.LogDebug(string.Format("Engine State updated FPS {0} Entitys count {1}", simIdleManager.SimulationEntityFPS, visualEntityManager.SimulationEntityCount));

            Interlocked.Decrement(ref simIdleManager.FPSRequestCount);

            simIdleManager.FPSUpdateResetEvent.Set();

            yield break;

        }

        /// <summary>
        /// Wait for either the simulation,physics etc engine to catch up, eg fps to an idle value
        /// </summary>
        public void WaitForIdle()
        {
            simIdleManager.WaitForIdle();
        }

        private void RequestFPSUpdate()
        {
            // spawn the updateing of the stats
            SpawnIterator(UpdateEngineState);
        }

        /// <summary>
        /// Check internal state of environment etc, make sure entity counts etc are all equal
        /// </summary>
        public void SanityCheck()
        {
            visualEntityManager.CheckEntityStates();
        }

        /// <summary>
        /// Simulation exception
        /// </summary>
        public class SimulationException : Exception
        {
            /// <summary>
            /// Simulation exception
            /// </summary>
            /// <param name="message"></param>
            public SimulationException(string message) : base(message) { }
        }
    }
}


